var CRYPTO = require("./crypto/crypto.js")
var utils = require("./util.js")
var ProxyPdu = require("./proxyPdu.js").ProxyPdu
var OpcodeParse = require("./opcode.js").OpcodeParse
var ControlOpcode = require("./opcode.js").ControlOpcode
var AccessOpcode = require("./opcode.js").AccessOpcode
var ConfigAccessOpcode = require("./opcode.js").ConfigAccessOpcode
var VendorAccessOpcode = require("./opcode.js").VendorAccessOpcode
var SegmentAcknowledgmentMessage = require("./opcode.js").SegmentAcknowledgmentMessage
var Timer = require("./timer.js")
var crypto = new CRYPTO()

const NonceType = {
  NetworkNonce: "00",
  ApplicationNonce: "01",
  DeviceNonce: "02",
  ProxyNonce: "03"
}

class UpperTransportAccessPDU {
  constructor(blemash) {
    this.blemash = blemash;
    this.accessPayload;
  }
  derive(accessPayload, dst, akf) {
    var ASZMIC = 0;
    var mic = utils.intToHex(ASZMIC << 7 & 0x80);
    var resutl;
    if (akf == 1) {
      var appNonce = NonceType.ApplicationNonce + mic + this.blemash.seq + this.blemash.src + dst + this.blemash.IVIndex;
      resutl = crypto.meshAuthEncAccessPayload(this.blemash.appKey, appNonce, accessPayload);
    } else {
      var deviceNonce = NonceType.DeviceNonce + mic + this.blemash.seq + this.blemash.src + dst + this.blemash.IVIndex;
      resutl = crypto.meshAuthEncAccessPayload(this.blemash.devKey, deviceNonce, accessPayload);
    }
    return resutl.EncAccessPayload + resutl.TransMIC;
  }
  parse(LowerTransportPDU, NetworkPDU, IVIndex) {
    IVIndex = IVIndex || this.blemash.IVIndex;
    var upperTransportPDU = LowerTransportPDU.upperTransportPDU;
    var ASZMIC = 0;
    var mic = utils.intToHex(ASZMIC << 7 & 0x80);
    var result;
    if (LowerTransportPDU.AKF == 1) {
      var appNonce = NonceType.ApplicationNonce + mic + NetworkPDU.FIRSTSEQ + NetworkPDU.SRC + NetworkPDU.DST + IVIndex;
      result = crypto.decryptAndVerify(this.blemash.appKey, upperTransportPDU, appNonce, LowerTransportPDU.SZMIC);
    } else {
      var deviceNonce = NonceType.DeviceNonce + mic + NetworkPDU.FIRSTSEQ + NetworkPDU.SRC + NetworkPDU.DST + IVIndex;
      result = crypto.decryptAndVerify(this.blemash.devKey, upperTransportPDU, deviceNonce, LowerTransportPDU.SZMIC);
    }
    if (result.status == -1) {
      if (IVIndex == this.blemash.IVIndex) {
        return this.parse(LowerTransportPDU, NetworkPDU, utils.toHex(parseInt(this.blemash.IVIndex, 16) - 1, 4));
      } 
      return false;
    }
    this.accessPayload = result.hex_decrypted;
    return true;
  }
}
class LowerTransportPDU {
  constructor(blemash) {
    this.blemash = blemash;
    this.SEG;
    this.AKF;
    this.AID;
    this.SZMIC;
    this.upperTransportPDU;
    this.acknowledgepdus;
    this.sequences;
    this.incompleteTimer;
    this.acknowledgmentTimer;
    this.transmissionTimer;
    this.failed;
    this.sendSegmentAcknowledgmentMessage;
    this.receiveSegmentAcknowledgmentMessage;
  }
  finish(n) {
    for (let i = 0; i <= n; i++) {
      if (this.sequences.indexOf(i) == -1) return false;
    }
    return true;
  }
  reassemble() {
    let pdu = "";
    for (let i = 0; i < this.sequences.length; i++) {
      pdu += this.acknowledgepdus[i];
    }
    this.upperTransportPDU = pdu;
  }
  deriveUnsegmentedAccessMessage(akf, UpperTransportPDU) {
    var result = [];
    var seg = 0;
    var SEG_AKF_AID;
    if (akf == 0) {
      SEG_AKF_AID = utils.intToHex((seg << 7) | (akf << 6) | (0 & 0x3F));
    } else {
      SEG_AKF_AID = utils.intToHex((seg << 7) | (akf << 6) | (this.blemash.aid & 0x3F));
    }
    result.push(SEG_AKF_AID + UpperTransportPDU);
    return result;
  }
  deriveSegmentedAccessMessage(akf, UpperTransportPDU) {
    var result = [];
    var seg = 1;
    var SZMIC = 0;
    var SegN = Math.ceil(UpperTransportPDU.length / 24) - 1;
    var SeqZero = utils.leastSignificantBit(this.blemash.seq, 13);
    var SEG_AKF_AID;
    if (akf == 0) {
      SEG_AKF_AID = utils.intToHex((seg << 7) | (akf << 6) | (0 & 0x3F));
    } else {
      SEG_AKF_AID = utils.intToHex((seg << 7) | (akf << 6) | (this.blemash.aid & 0x3F));
    }
    for (let i = 0; i <= SegN; i++) {
      let SZMIC_SeqZero = utils.intToHex((SZMIC << 7) | (SeqZero >> 6));
      let SeqZero_SegO = utils.intToHex(((SeqZero & 0x3F) << 2) | (i >> 3));
      let SegO_SegN = utils.intToHex((i & 0x7) << 5 | SegN);
      result.push(SEG_AKF_AID + SZMIC_SeqZero + SeqZero_SegO + SegO_SegN + UpperTransportPDU.substring(i * 24, i * 24 + 24));
    }
    return result;
  }
  deriveUnsegmentedControlMessage(opcode, parameters) {
    var result = [];
    var seg = 0;
    var SEG_Opcode = utils.intToHex((seg << 7) | (opcode & 0x7F));
    result.push(SEG_Opcode + parameters);
    return result;
  }
  deriveSegmentedControlMessage(opcode, parameters) {
    var result = [];
    var seg = 1;
    var SegN = Math.ceil(parameters.length / 16) - 1;
    var SeqZero = utils.leastSignificantBit(this.blemash.seq, 13);
    var SEG_Opcode = utils.intToHex((seg << 7) | (opcode & 0x7F));
    for (let i = 0; i <= SegN; i++) {
      let RFU_SeqZero = utils.intToHex(SeqZero >> 6);
      let SeqZero_SegO = utils.intToHex((SeqZero & 0x3F) | (i >> 2));
      let SegO_SegN = utils.intToHex((i & 0x3) | SegN);
      result.push(SEG_Opcode + RFU_SeqZero + SeqZero_SegO + SegO_SegN + parameters.substring(i * 12, i * 12 + 12));
    }
    return result;
  }
  derive(ctl, akf, UpperTransportPDU) {
    if (ctl == 0) {
      if (UpperTransportPDU.length > 30) {
        return this.deriveSegmentedAccessMessage(akf, UpperTransportPDU);
      } else {
        return this.deriveUnsegmentedAccessMessage(akf, UpperTransportPDU);
      }
    } else {
      var opcode = parseInt(UpperTransportPDU.substr(0, 2), 16);
      var parameters = UpperTransportPDU.substr(2);
      if (UpperTransportPDU.length > 22) {
        return this.deriveSegmentedControlMessage(opcode, parameters);
      } else {
        return this.deriveUnsegmentedControlMessage(opcode, parameters);
      }
    }
  }
  parse(NetworkPDU) {
    var lowerTransportPDU = NetworkPDU.TransportPDU;
    var seg_akf_aid = lowerTransportPDU.substring(0, 2);
    this.SEG = (parseInt(seg_akf_aid, 16) & 0x80) >> 7;
    this.AKF = (parseInt(seg_akf_aid, 16) & 0x40) >> 6;
    this.AID = parseInt(seg_akf_aid, 16) & 0x3F;
    var opcode = parseInt(seg_akf_aid, 16) & 0x7F;
    if (this.SEG == 0) {
      if (NetworkPDU.CTL == 0) {
        this.upperTransportPDU = lowerTransportPDU.substring(2, lowerTransportPDU.length);
        NetworkPDU.FIRSTSEQ = NetworkPDU.SEQ;
      } else {
        this.upperTransportPDU = utils.intToHex(opcode) + lowerTransportPDU.substring(2, lowerTransportPDU.length);
      }
      return true;
    } else {
      var SZMIC_SeqZero = lowerTransportPDU.substring(2, 4);
      var SeqZero_SegO = lowerTransportPDU.substring(4, 6);
      var SegO_SegN = lowerTransportPDU.substring(6, 8);
      var SeqZero = (SZMIC_SeqZero & 0x3F) << 6 | (parseInt(SeqZero_SegO, 16) >> 2);
      var SegN = parseInt(SegO_SegN, 16) & 0x1F;
      var SegO = ((parseInt(SeqZero_SegO, 16) & 0x3) << 3 | (parseInt(SegO_SegN, 16) >> 5));
      this.SZMIC = parseInt(SZMIC_SeqZero, 16) >> 7;
      if (LowerTransportPDU.prototype.SeqZero != SeqZero) {
        LowerTransportPDU.prototype.SeqZero = SeqZero;
        NetworkPDU.FIRSTSEQ = NetworkPDU.SEQ;
        this.acknowledgepdus = {};
        this.sequences = [];
        this.sendSegmentAcknowledgmentMessage = new SegmentAcknowledgmentMessage();
        this.sendSegmentAcknowledgmentMessage.obo = 0;
        this.sendSegmentAcknowledgmentMessage.seqZero = SeqZero;
        this.incompleteTimer = new Timer(Date.now(), 10000);
        this.incompleteTimer.schdule(() => {
          this.failed = true;
          this.incompleteTimer.close();
          this.acknowledgmentTimer.close();
        })
        this.acknowledgmentTimer = new Timer(Date.now(), 150 + 50 * NetworkPDU.TTL);
        this.acknowledgmentTimer.schdule(() => {
          this.blemash.sendSegmentAcknowledgmentMessage(this.sendSegmentAcknowledgmentMessage, NetworkPDU.SRC);
        })
      }
      if (this.failed) return false;
      if (this.sequences.indexOf(SegO) == -1) {
        this.sequences.push(SegO);
        this.sendSegmentAcknowledgmentMessage.blockAck.push(SegO);
        this.acknowledgepdus[SegO] = lowerTransportPDU.substring(8);
      }
      if (!this.finish(SegN)) return false;
      this.acknowledgmentTimer.nowSchdule();
      this.incompleteTimer.close();
      this.acknowledgmentTimer.close();
      this.reassemble();
      LowerTransportPDU.prototype.SeqZero = null;
      return true;
    }
  }
}

class NetworkPDU {
  constructor(blemash) {
    this.blemash = blemash;
    this.IVI;
    this.NID;
    this.CTL;
    this.TTL;
    this.SEQ;
    this.SRC;
    this.DST;
    this.FIRSTSEQ;
    this.TransportPDU;
  }
  derive(ctl, dst, lowerTransportPDU) {
    var CTL_TTL = utils.intToHex((ctl << 7 & 0x80 | this.blemash.ttl & 0x7F));
    var networkNonce = NonceType.NetworkNonce + CTL_TTL + this.blemash.seq + this.blemash.src + "0000" + this.blemash.IVIndex;
    var result = crypto.meshAuthEncNetwork(this.blemash.encryption_key, networkNonce, dst, lowerTransportPDU, ctl);
    var obfuscated = crypto.obfuscate(result.EncDST, result.EncTransportPDU, result.NetMIC, CTL_TTL,
      this.blemash.seq, this.blemash.src, this.blemash.IVIndex, this.blemash.privacyKey);
    var IVI_NID = utils.intToHex((this.blemash.ivi << 7 & 0x80) | this.blemash.NID, 16 & 0x7F);
    var pdu = IVI_NID + obfuscated.obfuscated_ctl_ttl_seq_src + result.EncDST + result.EncTransportPDU + result.NetMIC;
    return pdu;
  }
  parse(network_pdu, IVIndex) {
    IVIndex = IVIndex || this.blemash.IVIndex;
    this.IVI = parseInt(network_pdu.substring(0, 2), 16) >> 7 & 0x1;
    this.NID = parseInt(network_pdu.substring(0, 2), 16) & 0x7F;
    var obfuscated_ctl_ttl_seq_src = network_pdu.substring(2, 14);
    var dst = network_pdu.substring(14, 18);
    var transport_pdu = network_pdu.substring(18, network_pdu.length - 8);
    var netmic = network_pdu.substring(network_pdu.length - 8, network_pdu.length);

    var privacy_random = crypto.privacyRandom(dst, transport_pdu, netmic);
    var deobfuscated = crypto.deobfuscate(obfuscated_ctl_ttl_seq_src, IVIndex, this.blemash.netkey,
      privacy_random, this.blemash.privacyKey);
    var ctl_ttl_seq_src = deobfuscated.ctl_ttl_seq_src;
    var ctl_ttl = ctl_ttl_seq_src.substring(0, 2);
    this.CTL = (parseInt(ctl_ttl, 16) & 0x80) >> 7;
    this.TTL = parseInt(ctl_ttl, 16) & 0x7F;
    this.SEQ = ctl_ttl_seq_src.substring(2, 8);
    this.SRC = ctl_ttl_seq_src.substring(8, 12);

    var networkNonce = "00" + ctl_ttl_seq_src + "0000" + IVIndex;
    var result = crypto.decryptAndVerify(this.blemash.encryption_key, network_pdu.substring(14, network_pdu.length), networkNonce, this.CTL);
    if (result.status == -1) {
      if (IVIndex == this.blemash.IVIndex) {
        return this.parse(network_pdu, utils.toHex(parseInt(this.blemash.IVIndex, 16) - 1, 4));
      } 
      return false;;
    }
    this.DST = result.hex_decrypted.substring(0, 4);
    this.TransportPDU = result.hex_decrypted.substring(4, result.hex_decrypted.length);
    return true;
  }
}


class BleCommand {
  constructor(blemash) {
    this.blemash = blemash;
    var k2_material = crypto.k2(blemash.netkey, "00");
    this.networkId = crypto.k3(blemash.netkey);
    this.netkey = blemash.netkey;
    this.appKey = blemash.appkey;
    this.netkeyIndex = blemash.netkeyIndex;
    this.appkeyIndex = blemash.appkeyIndex;
    this.aid = parseInt(crypto.k4(blemash.appkey), 16);
    this.IVIndex = blemash.iv_index;
    this.NID = parseInt(k2_material.NID, 16);
    this.privacyKey = k2_material.privacy_key;
    this.encryption_key = k2_material.encryption_key;
    this.ivi = utils.leastSignificantBit(blemash.iv_index, 1);
    this.ttl = blemash.ttl;
    this.seq = blemash.seq_number;
    this.src = blemash.src;
    this.upperTransportAccessPDU = new UpperTransportAccessPDU(this);
    this.lowerTransportPDU = new LowerTransportPDU(this);
    this.networkPdu = new NetworkPDU(this);
  }
  isUnicastAddress(address) {
    return parseInt(address, 16) >> 15 == 0
  }
  increamentSEQ() {
    if (this.seq == "ffffff") {
      this.seq = "000000";
      this.blemash.ivUpdate();
      this.ivi = utils.leastSignificantBit(blemash.iv_index, 1);
    } else {
      this.seq = utils.toHex(parseInt(this.seq, 16) + 1, 3);
    }
  }
  setDevKey(devKey) {
    this.devKey = devKey;
  }
  receiveNetworkPdu(pdu) {
    if(!this.networkPdu.parse(pdu)){
      BleCommand.prototype.callback("12", this.networkPdu.SRC);
      return;
    }
    var finish = this.lowerTransportPDU.parse(this.networkPdu, this.networkPdu);
    if (finish) {
      if (this.networkPdu.CTL == 0) {
        if(!this.upperTransportAccessPDU.parse(this.lowerTransportPDU, this.networkPdu)){
          BleCommand.prototype.callback("13", this.networkPdu.SRC);
          return;
        }
        var op = OpcodeParse.parse(this.networkPdu.CTL, this.upperTransportAccessPDU.accessPayload);
      } else {
        var op = OpcodeParse.parse(this.networkPdu.CTL, this.lowerTransportPDU.upperTransportPDU);
      }
      if (!op.transportControl || !op.transportControl.type == "SegmentAcknowledgmentMessage") {
        if (BleCommand.prototype.callback) {
          BleCommand.prototype.callback(op.access, this.networkPdu.SRC);
        }
      } else {
        this.lowerTransportPDU.receiveSegmentAcknowledgmentMessage = op.transportControl;
        this.lowerTransportPDU.transmissionTimer.nowSchdule();
      }
    }
  }
  sendNetworkPdu(ctl, dst, lowerTransportPDU) {
    var networkPdu = this.networkPdu.derive(ctl, dst, lowerTransportPDU);
    this.increamentSEQ();
    var proxyPdu = ProxyPdu.prototype.deriveNetworkPdu(networkPdu);
    this.blemash.writeProxyValue(proxyPdu);
  }
  sendLowerTransportPDU(ctl, akf, upperTransportAccessPDU, dst) {
    var lowerTransportPDU = this.lowerTransportPDU.derive(ctl, akf, upperTransportAccessPDU);
    if (this.isUnicastAddress(dst) && lowerTransportPDU.length > 1) {
      this.lowerTransportPDU.transmissionTimer = new Timer(Date.now(), 1000 + 50 * this.ttl);
      this.lowerTransportPDU.transmissionTimer.schdule(() => {
        let array1 = utils.range(lowerTransportPDU.length);
        let array2;
        if (this.lowerTransportPDU.receiveSegmentAcknowledgmentMessage) {
          array2 = this.lowerTransportPDU.receiveSegmentAcknowledgmentMessage.blockAck;
        } else {
          array2 = [];
        }
        let array = utils.arrayUnion(array1, array2);
        if (array.length == 0) {
          this.lowerTransportPDU.transmissionTimer.close();
        } else {
          for (let i in array) {
            this.sendNetworkPdu(ctl, dst, lowerTransportPDU[i]);
          }
        }
      })
    }
    for (let i = 0; i < lowerTransportPDU.length; i++) {
      this.sendNetworkPdu(ctl, dst, lowerTransportPDU[i]);
    }
    this.blemash.emit("seqChange", this.blemash.meshId, this.seq);
  }
  sendAccessOpcode(dst, akf, accesspdu) {
    var upperTransportAccessPDU = this.upperTransportAccessPDU.derive(accesspdu, dst, akf);
    this.sendLowerTransportPDU(0, akf, upperTransportAccessPDU, dst)
  }
  sendControlOpcode(dst, upperTransportAccessPDU) {
    this.sendLowerTransportPDU(1, 0, upperTransportAccessPDU, dst);
  }
  sendSegmentAcknowledgmentMessage(opcode, dst) {
    if (opcode instanceof ControlOpcode) {
      this.sendControlOpcode(dst, opcode.toPdu());
    } else if (opcode instanceof VendorAccessOpcode) {
      this.sendAccessOpcode(dst, 1, opcode.toPdu());
    } else if (opcode instanceof ConfigAccessOpcode) {
      this.sendAccessOpcode(dst, 0, opcode.toPdu());
    }
  }
  sendOpcode(opcode, dst, callback) {
    if (opcode instanceof ControlOpcode) {
      this.sendControlOpcode(dst, opcode.toPdu());
    } else if (opcode instanceof VendorAccessOpcode) {
      this.sendAccessOpcode(dst, 1, opcode.toPdu());
    } else if (opcode instanceof ConfigAccessOpcode) {
      this.sendAccessOpcode(dst, 0, opcode.toPdu());
    }
    BleCommand.prototype.callback = callback;
  }
  
  cancle() {
    if (this.lowerTransportPDU.transmissionTimer) {
      this.lowerTransportPDU.transmissionTimer.close();
    }
  }

}

module.exports = BleCommand;